from collections.abc import Iterable

from django.core.exceptions import ObjectDoesNotExist
from django.core.files import File
from rest_framework import serializers

from sysreptor import signals as sysreptor_signals
from sysreptor.pentests.import_export.serializers.common import (
    ExportImportSerializer,
    FileExportImportSerializer,
    MultiFormatSerializer,
)
from sysreptor.pentests.import_export.serializers.notes import (
    NotebookPageExportImportSerializer,
    NotebookPageListExportImportSerializer,
)
from sysreptor.pentests.import_export.serializers.project_type import (
    ProjectTypeExportImportSerializer,
)
from sysreptor.pentests.models import (
    FindingTemplate,
    PentestFinding,
    PentestProject,
    ProjectMemberInfo,
    ReportSection,
    SourceEnum,
    UploadedImage,
    UploadedProjectFile,
)
from sysreptor.pentests.models.project import Comment, CommentAnswer
from sysreptor.pentests.serializers.project import ProjectMemberInfoSerializer, TextRangeSerializer
from sysreptor.users.models import PentestUser
from sysreptor.users.serializers import RelatedUserSerializer
from sysreptor.utils.fielddefinition.utils import (
    HandleUndefinedFieldsOptions,
    ensure_defined_structure,
    get_field_value_and_definition,
)
from sysreptor.utils.history import merge_with_previous_history
from sysreptor.utils.serializers import OptionalPrimaryKeyRelatedField


class UserIdSerializer(serializers.ModelSerializer):
    class Meta:
        model = PentestUser
        fields = ['id']


class RelatedUserIdExportImportSerializer(RelatedUserSerializer):
    def __init__(self, **kwargs):
        super().__init__(user_serializer=UserIdSerializer, **{'required': False, 'allow_null': True, 'default': None} | kwargs)

    def to_internal_value(self, data):
        try:
            return super().to_internal_value(data)
        except serializers.ValidationError as ex:
            if isinstance(ex.__cause__, ObjectDoesNotExist):
                # If user does not exit: ignore
                raise serializers.SkipField() from ex
            else:
                raise


class UserDataSerializer(serializers.ModelSerializer):
    class Meta:
        model = PentestUser
        fields = [
            'id', 'email', 'phone', 'mobile',
            'username', 'name', 'title_before', 'first_name', 'middle_name', 'last_name', 'title_after',
        ]
        extra_kwargs = {'id': {'read_only': False}}


class RelatedUserDataExportImportSerializer(ProjectMemberInfoSerializer):
    def __init__(self, **kwargs):
        super().__init__(user_serializer=UserDataSerializer, **kwargs)

    def to_internal_value(self, data):
        try:
            return ProjectMemberInfo(**super().to_internal_value(data))
        except serializers.ValidationError as ex:
            if isinstance(ex.__cause__, ObjectDoesNotExist):
                return data
            else:
                raise


class ProjectMemberListExportImportSerializer(serializers.ListSerializer):
    child = RelatedUserDataExportImportSerializer()

    def to_representation(self, project):
        return super().to_representation(project.members.all()) + project.imported_members

    def to_internal_value(self, data):
        return {self.field_name: super().to_internal_value(data)}


class UploadedProjectImageExportImportSerializer(FileExportImportSerializer):
    class Meta(FileExportImportSerializer.Meta):
        model = UploadedImage

    def get_linked_object(self):
        return self.context['project']

    def is_file_referenced(self, f):
        return self.get_linked_object().is_file_referenced(f, findings=True, sections=True, notes=self.context.get('export_all', True))

    def get_path_in_archive(self, name):
        # Get ID of old project_type from archive
        return str(self.context.get('project_id') or self.get_linked_object().id) + '-images/' + name


class UploadedProjectFileExportImportSerializer(FileExportImportSerializer):
    class Meta(FileExportImportSerializer.Meta):
        model = UploadedProjectFile

    def get_linked_object(self):
        return self.context['project']

    def get_path_in_archive(self, name):
        # Get ID of old project_type from archive
        return str(self.context.get('project_id') or self.get_linked_object().id) + '-files/' + name



class CommentAnswerExportImportSerializer(ExportImportSerializer):
    user = RelatedUserIdExportImportSerializer()

    class Meta:
        model = CommentAnswer
        fields = ['id', 'created', 'updated', 'user', 'text']
        extra_kwargs = {'created': {'read_only': False, 'required': False}}


class CommentExportImportSerializer(ExportImportSerializer):
    user = RelatedUserIdExportImportSerializer()
    answers = CommentAnswerExportImportSerializer(many=True)
    text_range = TextRangeSerializer(allow_null=True)
    path = serializers.CharField(source='path_absolute')

    class Meta:
        model = Comment
        fields = [
            'id', 'created', 'updated', 'user', 'path',
            'text_range', 'text_original', 'text', 'answers',
        ]
        extra_kwargs = {'created': {'read_only': False, 'required': False}}

    def get_obj_and_path(self, path_absolute):
        path_parts = path_absolute.split('.')
        if len(path_parts) < 4 or path_parts[0] not in ['findings', 'sections'] or path_parts[2] != 'data':
            raise serializers.ValidationError('Invalid path')

        obj = None
        if path_parts[0] == 'findings':
            obj = next(filter(lambda f: str(f.finding_id) == path_parts[1], self.context['project'].findings.all()), None)
        elif path_parts[0] == 'sections':
            obj = next(filter(lambda s: str(s.section_id) == path_parts[1], self.context['project'].sections.all()), None)
        if not obj:
            raise serializers.ValidationError('Invalid path')

        try:
            get_field_value_and_definition(data=obj.data, definition=obj.field_definition, path=path_parts[3:])
        except KeyError as ex:
            raise serializers.ValidationError('Invalid path') from ex

        return obj, '.'.join(path_parts[2:])

    def create(self, validated_data):
        obj, path = self.get_obj_and_path(validated_data.pop('path_absolute'))

        answers = validated_data.pop('answers', [])
        comment = super().create(validated_data | {
            'path': path,
            'finding': obj if isinstance(obj, PentestFinding) else None,
            'section': obj if isinstance(obj, ReportSection) else None,
        })
        CommentAnswer.objects.bulk_create([CommentAnswer(comment=comment, **a) for a in answers])
        return comment


class PentestFindingExportImportSerializer(ExportImportSerializer):
    id = serializers.UUIDField(source='finding_id')
    assignee = RelatedUserIdExportImportSerializer()
    template = OptionalPrimaryKeyRelatedField(queryset=FindingTemplate.objects.all(), source='template_id')
    data = serializers.DictField()

    class Meta:
        model = PentestFinding
        fields = [
            'id', 'created', 'updated', 'assignee', 'status', 'template', 'order', 'data',
        ]
        extra_kwargs = {'created': {'read_only': False, 'required': False}}

    def create(self, validated_data):
        project = self.context['project']
        data = validated_data.pop('data', {})
        template = validated_data.pop('template_id', None)

        return PentestFinding.objects.create(**{
            'project': project,
            'template_id': template.id if template else None,
            'data': ensure_defined_structure(
                value=data,
                definition=project.project_type.finding_fields_obj,
                handle_undefined=HandleUndefinedFieldsOptions.FILL_NONE,
                include_unknown=True),
        } | validated_data)


class ReportSectionExportImportSerializer(ExportImportSerializer):
    id = serializers.CharField(source='section_id')
    assignee = RelatedUserIdExportImportSerializer()

    class Meta:
        model = ReportSection
        fields = [
            'id', 'created', 'updated', 'assignee', 'status',
        ]
        extra_kwargs = {'created': {'read_only': False, 'required': False}}

    def update(self, instance, validated_data):
        instance.skip_history_when_saving = True
        out = super().update(instance, validated_data)
        del instance.skip_history_when_saving

        # Add changes to previous history record to have a clean history timeline (just one entry for import)
        merge_with_previous_history(instance)

        return out


class ProjectNotebookPageExportImportSerializer(NotebookPageExportImportSerializer):
    assignee = RelatedUserIdExportImportSerializer()

    class Meta(NotebookPageExportImportSerializer.Meta):
        fields = NotebookPageExportImportSerializer.Meta.fields + ['assignee']
        extra_kwargs = NotebookPageExportImportSerializer.Meta.extra_kwargs | {
            'assignee': {'required': False},
        }


class PentestProjectExportImportSerializerV1(ExportImportSerializer):
    members = ProjectMemberListExportImportSerializer(source='*', required=False)
    pentesters = ProjectMemberListExportImportSerializer(required=False, write_only=True)
    project_type = ProjectTypeExportImportSerializer()
    report_data = serializers.DictField(source='data')
    sections = ReportSectionExportImportSerializer(many=True)
    findings = PentestFindingExportImportSerializer(many=True)
    notes = NotebookPageListExportImportSerializer(child=ProjectNotebookPageExportImportSerializer(), required=False)
    images = UploadedProjectImageExportImportSerializer(many=True)
    files = UploadedProjectFileExportImportSerializer(many=True, required=False)
    comments = CommentExportImportSerializer(many=True, required=False)

    class Meta:
        model = PentestProject
        fields = [
            'id', 'created', 'updated',
            'name', 'language', 'tags', 'override_finding_order', 'report_data',
            'members', 'pentesters', 'project_type',
            'sections', 'findings', 'notes', 'images', 'files', 'comments',
        ]
        extra_kwargs = {
            'id': {'read_only': False},
            'created': {'read_only': False, 'required': False},
            'tags': {'required': False},
        }

    def get_fields(self):
        fields = super().get_fields()
        if not self.context.get('export_all', True):
            del fields['notes']
            del fields['files']
        return fields

    def to_representation(self, instance):
        self.context.update({'project': instance})
        return super().to_representation(instance)

    def export_files(self, instance) -> Iterable[tuple[str, File]]:
        yield from self.fields['project_type'].export_files(instance=instance.project_type)

        self.context.update({'project': instance})

        imgf = self.fields['images']
        yield from imgf.export_files(instance=list(imgf.get_attribute(instance).all()))

        if ff := self.fields.get('files'):
            yield from ff.export_files(instance=list(ff.get_attribute(instance).all()))

    def create(self, validated_data):
        old_id = validated_data.pop('id')
        members = validated_data.pop('members', validated_data.pop('pentesters', []))
        project_type_data = validated_data.pop('project_type', {})
        sections = validated_data.pop('sections', [])
        findings = validated_data.pop('findings', [])
        notes = validated_data.pop('notes', [])
        report_data = validated_data.pop('data', {})
        images_data = validated_data.pop('images', [])
        files_data = validated_data.pop('files', [])
        comments_data = validated_data.pop('comments', [])

        project_type = self.fields['project_type'].create(project_type_data | {
            'source': SourceEnum.IMPORTED_DEPENDENCY,
        })
        project = super().create(validated_data | {
            'project_type': project_type,
            'imported_members': list(filter(lambda u: isinstance(u, dict), members)),
            'source': SourceEnum.IMPORTED,
            'unknown_custom_fields': ensure_defined_structure(
                value=report_data,
                definition=project_type.all_report_fields_obj,
                handle_undefined=HandleUndefinedFieldsOptions.FILL_NONE,
                include_unknown=True,
            ),
            'skip_post_create_signal': True,
        })
        project_type.linked_project = project
        project_type.save()
        project.set_members(list(filter(lambda u: isinstance(u, ProjectMemberInfo), members)), new=True)

        self.context.update({'project': project, 'project_id': old_id})

        for section in project.sections.all():
            if section_data := next(filter(lambda s: s.get('section_id') == section.section_id, sections), None):
                self.fields['sections'].child.update(section, section_data)

        self.fields['findings'].create(findings)
        self.fields['images'].create(images_data)
        self.fields['comments'].create(comments_data)
        if notesf := self.fields.get('notes'):
            notesf.create(notes)
        if filesf := self.fields.get('files'):
            filesf.create(files_data)

        sysreptor_signals.post_create.send(sender=project.__class__, instance=project)

        return project


class PentestProjectExportImportSerializer(MultiFormatSerializer):
    # Previous implementation required "projecttypes/v2" for "projects/v2"
    # Since the current implementation can hanvle both formats for sub-serializers, v1 and v2 can use the same serializer.
    serializer_formats = {
        'projects/v2': PentestProjectExportImportSerializerV1(),
        'projects/v1': PentestProjectExportImportSerializerV1(),
    }

